feat(cli): print session summary at end of exec runs#3278
Conversation
Non-interactive runs previously emitted no usage recap; the TUI only had the on-demand /cost dialog. This adds an end-of-run summary covering the interaction (tool calls, success rate), per-model token usage, and total cost. The recap is derived from the existing session via a new Session.Stats() helper (tool-call success/failure counts, per-model token and cost breakdown, cache savings, duration). It is written to stderr so stdout stays machine-consumable, and is skipped in JSON output mode. Refs #1345
|
@Sayt-0 add a |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
One medium-severity issue found in the new Session.Stats() implementation. The overall PR structure is sound — stderr output, JSON mode guard, no-op for empty sessions, and correct token math are all well-implemented.
| switch { | ||
| case item.IsMessage(): | ||
| m := item.Message.Message | ||
| st.Cost += m.Cost |
There was a problem hiding this comment.
[MEDIUM] Potential cost double-counting in Stats() remote-mode fallback
st.Cost += m.Cost is accumulated unconditionally for every message during the walk (line 113), including assistant messages where m.Usage == nil. However, st.Requests is only incremented when m.Usage != nil. In remote mode, usage is stored in MessageUsageHistory rather than on the message object — so it is architecturally possible for an assistant message to carry m.Cost > 0 while m.Usage == nil.
When this happens:
- The walk adds the message's cost to
st.Cost(via line 113). - After the walk,
st.Requests == 0→ the remote-mode fallback fires. - The fallback also adds
r.Costfor the same call fromMessageUsageHistory.
The cost is counted twice. The fix is either to skip the message-level cost accumulation when m.Usage == nil (since that cost will be covered by the fallback path), or to zero out st.Cost before entering the fallback loop:
// Option A — only accumulate m.Cost when usage is present:
case chat.MessageRoleAssistant:
if m.Usage != nil {
st.Cost += m.Cost // move cost inside the usage guard
st.Requests++
// ...
}
// Option B — reset before fallback:
if st.Requests == 0 && len(s.MessageUsageHistory) > 0 {
st.Cost = 0 // discard costs collected without usage data
for _, r := range s.MessageUsageHistory { ... }
}
Summary
Adds an end-of-run session summary for non-interactive (
--exec) runs. Until now--execprinted no usage recap, while the TUI only exposed the on-demand/costdialog. The recap covers interaction stats (tool calls, success rate), per-model token usage, total cost, and cache savings, matching the request in #1345.The data is derived from the existing session through a new
Session.Stats()helper, so no new tracking pipeline or protocol events are introduced.Sample output
Mapping to the request (#1345)
Design notes
TestExec_*tests) is unchanged.Validation
task buildgolangci-lintonpkg/session,pkg/clipkg/session,pkg/cli)TestExec_*)Out of scope / possible follow-ups
2>/dev/null).